﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using CacheProviderEntities;
using CacheProviderInterfaces;
using Microsoft.Practices.Composite.Presentation.Events;
using Oracle.DataAccess.Client;
using System.Data.Common;
using CacheProviderUtilities;

namespace CacheProvider
{
    public class OracleMonitoringManager : MonitoringManager<OracleDependency>
    {
        #region C O N S T R U C T O R (S)

        public OracleMonitoringManager(CacheProvider cacheProvider, IDependencyInjectionContainer dependencyInjectionContainer)
            : base(cacheProvider, dependencyInjectionContainer)
        {

        }

        #endregion

        #region P U B L I C   M E M B E R (S)

        /// <summary>
        /// Sets up monitoring for Dependency Management.
        /// </summary>
        /// <param name="dbConnectionString">SQL Server DB connection String for which to monitor dependencies</param>
        /// <returns>Returns <see cref="CacheDependencyStatus"/></returns>
        public override CacheDependencyStatus StartMonitoring(string dbConnectionString)
        {
            var monitoringStatus = CacheDependencyStatus.Unknown;
            MonitoringInfo monitoringInfo = null;
            string formattedConnectionString = this.GetFormattedConnectionString(dbConnectionString);

            if (string.IsNullOrEmpty(formattedConnectionString))
            {
                monitoringStatus = CacheDependencyStatus.InvalidConnectionString;
            }
            else
            {
                lock (DictionarySerializer)
                {
                    //Check whether SqlMonitoring already started for the given connection string.
                    //In this case, monitoring status will be returned as CacheDependencyStatus.AlreadyMonitoring
                    if (_monitoringDictionary.Contains(formattedConnectionString))
                    {
                        monitoringInfo = _monitoringDictionary[formattedConnectionString] as MonitoringInfo;

                        if (monitoringInfo != null)
                            if (monitoringInfo.Status == CacheDependencyStatus.MonitoringStarted)
                            {
                                monitoringStatus = CacheDependencyStatus.AlreadyMonitoring;
                                return monitoringStatus;
                            }
                    }

                    monitoringInfo = new MonitoringInfo()
                    {
                        Status = CacheDependencyStatus.MonitoringStarted,
                        MonitoringStartTime = DateTime.UtcNow

                    };
                    if (_monitoringDictionary.Contains(formattedConnectionString))
                    {
                        this._monitoringDictionary[formattedConnectionString] = monitoringInfo;
                    }
                    else
                    {
                        this._monitoringDictionary.Add(formattedConnectionString, monitoringInfo);
                    }
                    monitoringStatus = CacheDependencyStatus.MonitoringStarted;
                }
            }

            return monitoringStatus;
        }

        /// <summary>
        /// Sets up monitoring for ExtendedOracleDependency Changes.
        /// </summary>
        /// <param name="dbConnectionString">Oracle connection String for which to stop monitoring dependencies</param>
        /// <returns>Returns <see cref="CacheDependencyStatus"/></returns>
        public override CacheDependencyStatus StopMonitoring(string dbConnectionString)
        {
            var monitoringStatus = CacheDependencyStatus.Unknown;
            string formattedConnectionString = this.GetFormattedConnectionString(dbConnectionString);

            if (string.IsNullOrEmpty(formattedConnectionString))
            {
                monitoringStatus = CacheDependencyStatus.InvalidConnectionString;
            }
            else
            {
                lock (DictionarySerializer)
                {
                    if (_monitoringDictionary.Contains(formattedConnectionString))
                    {
                        var monitoringInfo = _monitoringDictionary[formattedConnectionString] as MonitoringInfo;
                        if (monitoringInfo != null)
                        {
                            _monitoringDictionary.Remove(formattedConnectionString);
                            monitoringStatus = CacheDependencyStatus.MonitoringStopped;
                        }
                    }
                    else
                    {
                        monitoringStatus = CacheDependencyStatus.NotRegistered;
                    }
                }
            }

            return monitoringStatus;
        }

        /// <summary>
        /// Registers SqlDependencies for monitoring changes. Has provision for
        /// automatic cache updation (when dependencies change) through callback registration.
        /// </summary>
        /// <typeparam name="T">Type of Payload with which the callback should be invoked.</typeparam>
        /// <param name="key">Cache key</param>
        /// <param name="namedCache">Name of the Named cache (if any & supported)</param>
        /// <param name="regionName">If Region is supported, Name of the Region where the key\value will remain</param>
        /// <param name="dependencyInfo">An instance of <see cref="DependencyInfo" with <see cref="DependencyInfo.DependencyTypes.SQLDependency"/>/> and other suitable properties set.</param>
        /// <param name="encacheDependencies">Whether to store the dependencies in the Cache.  Useful for Distributed cache providers to provide dependency management fallback.</param>
        /// <param name="cacheUpdationCallback">The callback delegate to invoke in case dependencies change.</param>
        /// <param name="callbackState">Payload instance with which the callback should be invoked.</param>
        /// <param name="threadOption">Thread on which the callback should be invoked. See <see cref="ThreadOption"/> for details</param>
        /// <param name="keepSubscriberReferenceAlive">Should there be a strong or weak reference to the callback.</param>
        /// <param name="delayForProcessingDependencyChange">Timespan to signify delay to be introduced while processing a Dependency Change notification.  If it used
        /// so that the win Service does not process before the client(if it is alive) because the client can invoke automatic update callbacks.</param>
        public override void AddSqlDependencies<T>(string key,
                                        string namedCache,
                                        string regionName,
                                        DependencyInfo dependencyInfo,
                                        bool encacheDependencies,
                                        Action<T> cacheUpdationCallback,
                                        T callbackState,
                                        ThreadOption threadOption,
                                        bool keepSubscriberReferenceAlive,
                                        TimeSpan delayForProcessingDependencyChange,
                                        Object cacheItemVersionData
            )
        {

            if (dependencyInfo == null)
                return;
            if (dependencyInfo.CacheDependencyType != CacheDependencyTypes.SQLDependency)
                throw new ArgumentException("dependencyInfo.CacheDependencyType");
            if (string.IsNullOrEmpty(dependencyInfo.DBConnectionString))
            {
                var cacheProviderErrorEventArgs = new CacheProviderErrorEventArgs()
                {
                    Sender = this,
                    CacheKey = key,
                    ErrorMessage = "DependencyInfo.DBConnectionString property is null"
                };


                this._cacheProvider.EventAggregatorInstance.GetEvent<CacheProviderErrorEvent>().Publish(cacheProviderErrorEventArgs);

                return;
            }

            var oracleDependency = new OracleDependency();
            var cacheDependencyTracker = new CacheDependencyTracker()
            {
                Key = key,
                DependencyData = dependencyInfo,
                NamedCache = namedCache,
                RegionName = regionName,
                AreDependenciesCached = encacheDependencies,
                DelayInDependencyNotificationProcessing = delayForProcessingDependencyChange,
                DependencyInstanceID = oracleDependency.Id
            };
            var oracleCommands = new List<OracleCommand>();
            oracleDependency.OnChange += OracleDependency_OnChange;

            #region SQL QUERIES PROCESSING

            if (!string.IsNullOrEmpty(dependencyInfo.DBConnectionString)
                && dependencyInfo.IsSQLSelectQueryBased
                && dependencyInfo.SelectQueries != null && dependencyInfo.SelectQueries.Count > 0
                )
            {
                foreach (var sqlQuery in dependencyInfo.SelectQueries)
                {
                    var oracleCommand = new OracleCommand(sqlQuery);
                    oracleCommands.Add(oracleCommand);
                    oracleDependency.AddCommandDependency(oracleCommand);
                    oracleCommand.Notification.IsNotifiedOnce = true;
                }
            }

            #endregion

            #region SQL TABLES PROCESSING

            if (!string.IsNullOrEmpty(dependencyInfo.DBConnectionString)
                && dependencyInfo.IsSQLTableBased
                && dependencyInfo.SQLTables != null && dependencyInfo.SQLTables.Count > 0)
            {
                using (var connection = new OracleConnection(dependencyInfo.DBConnectionString))
                {
                    foreach (var table in dependencyInfo.SQLTables)
                    {
                        var sqlQuery = GetSQLQuery(connection, table);

                        var oracleCommand = new OracleCommand(sqlQuery);
                        oracleCommands.Add(oracleCommand);
                        oracleDependency.AddCommandDependency(oracleCommand);
                        oracleCommand.Notification.IsNotifiedOnce = true;
                    }
                }
            }

            #endregion

            #region SQL COMMANDS PROCESSING

            if (!string.IsNullOrEmpty(dependencyInfo.DBConnectionString)
               && dependencyInfo.ISDbCommandBased
               && dependencyInfo.DbCommands != null && dependencyInfo.DbCommands.Count > 0)
            {

                using (var connection = new OracleConnection(dependencyInfo.DBConnectionString))
                {
                    foreach (var dbCommand in dependencyInfo.DbCommands)
                    {
                        var oracleCommand = dbCommand as OracleCommand;
                        Debug.Assert(oracleCommand != null, "DBCommand must be a SqlCommand here!");
                        dbCommand.Connection = connection;
                        oracleDependency.AddCommandDependency(oracleCommand);
                        oracleCommand.Notification.IsNotifiedOnce = true;
                        oracleCommands.Add(oracleCommand);
                    }
                }
            }

            #endregion


            if (cacheDependencyTracker == null) return;

            this._dependenciesCollection.Add(oracleDependency);

            bool wasDepTrackerAddedSuccessfully;

            try
            {
                DependencyTrackerLocker.EnterWriteLock();

                //This is required as the Cache Server does not remove an
                //item from Cache as soon as it expires.   Due to that, whenever
                //it removes the item, then, only it raises the notification.  Since,
                // we depend on notification from the Cache Server, the user may try 
                // to add the same key again in between with same\different value.
                this.RemoveItemFromDependencyTracker(key, namedCache, regionName, null);

                this._cacheDependencyTracker.Add(cacheDependencyTracker);

                if (cacheItemVersionData != null)
                    cacheDependencyTracker.CacheItemDataVersion = cacheItemVersionData;

                if (encacheDependencies)
                    ThreadPool.QueueUserWorkItem(EncacheDependencies);

                wasDepTrackerAddedSuccessfully = true;
            }
            finally
            {
                DependencyTrackerLocker.ExitWriteLock();
            }

            #region SQL DEPENDENCY COMMMANDS EXECUTION

            //commands are executed only after the Dependency Tracker list is updated to avoid a race
            //condition where dependency change notification may be triggered even before the Dependency Tracker is updated.
            if (wasDepTrackerAddedSuccessfully && oracleCommands.Count() > 0 && dependencyInfo.DBConnectionString != null)
            {
                using (var connection = new OracleConnection(dependencyInfo.DBConnectionString))
                {
                    connection.Open();
                    foreach (var sqlCommand in oracleCommands)
                    {
                        sqlCommand.Connection = connection;
                        bool noOfRetryExceeds;
                        var noOfRetry = 0;
                        //Do-While loop till query registration succeeded.
                        do
                        {
                            try
                            {
                                sqlCommand.ExecuteNonQuery();
                                noOfRetryExceeds = true;
                            }
                            catch (SqlException exception)
                            {
                                //Deadlock exception number: 1205.we should retry the registration
                                if (exception.Number != 1205)
                                    throw;
                                if (noOfRetry == 0)
                                {
                                    var message = string.Format("{0} - query registration enters into transaction deadlock.",
                                                            sqlCommand.CommandText);
                                    this._logWriter.Write(message, this._cacheProvider.LoggingCategory, 1,
                                                          (int)LoggingEvent.CacheNotification, TraceEventType.Warning, key);
                                }
                                //Following code will retry the query registration with delay of 
                                //(2^n * 1000) seconds where n is current count of retry. It will retry for maximum
                                //of five time. This time delay is required to make previous transaction to be completed.
                                //Otherwise in subsequent attempts, registration will end up in deadlock situation.
                                var delayTime = Math.Pow(2, noOfRetry) * 1000;
                                Thread.Sleep((int)delayTime);
                                noOfRetry++;
                                noOfRetryExceeds = (noOfRetry == this._executeRetryCount);
                                //If registration fails after 5 attempts, SqlMontoringManager will
                                //log the error message and continue with other dependency registration.
                                if (noOfRetryExceeds)
                                {
                                    var message = string.Format("{0} - Query fails to register the dependency for key: {1}", sqlCommand.CommandText, key);
                                    this._logWriter.Write(message, this._cacheProvider.LoggingCategory, 1,
                                                          (int)LoggingEvent.CacheNotification, TraceEventType.Critical, key);
                                }
                            }
                        }
                        while (!noOfRetryExceeds);
                    }
                }
            }

            #endregion

            //Register callback for automatic cache updatation.
            if (wasDepTrackerAddedSuccessfully && cacheUpdationCallback != null)
                cacheDependencyTracker.CallbackManager.RegisterCallback(cacheUpdationCallback,
                                                                        callbackState,
                                                                        threadOption,
                                                                        keepSubscriberReferenceAlive);
        }

        /// <summary>
        ///     
        /// </summary>
        /// <param name="id"></param>
        public override void RemoveChangeNotification(string id)
        {
            if (_dependenciesCollection == null || _dependenciesCollection.Count == 0) return;

            var sqlDependency = this._dependenciesCollection.Where(d => (d as OracleDependency).Id == id).FirstOrDefault();

            if (sqlDependency == null) return;

            sqlDependency.OnChange -= OracleDependency_OnChange;
            this._dependenciesCollection.Remove(sqlDependency);
        }

        #endregion

        #region P R I V A T E  M E T H O D (S)

        /// <summary>
        ///     
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OracleDependency_OnChange(object sender, OracleNotificationEventArgs e)
        {
            QueryNotificationState queryNotificationInfo = null;

            switch (e.Info)
            {
                case OracleNotificationInfo.Error:
                case OracleNotificationInfo.Shutdown:
                case OracleNotificationInfo.Shutdown_any:
                case OracleNotificationInfo.Startup:
                case OracleNotificationInfo.End:
                    CacheDependencyTracker dependencyData = null;
                    try
                    {
                        DependencyTrackerLocker.EnterWriteLock();

                        if (sender is OracleDependency)
                        {
                            dependencyData = (from dependencyInfo in this._cacheDependencyTracker
                                              where dependencyInfo.DependencyInstanceID.Equals((sender as OracleDependency).Id)
                                              select dependencyInfo).FirstOrDefault();
                        }

                        if (dependencyData != null && this._cacheProvider != null)
                        {
                            this._cacheProvider.Remove(dependencyData.Key);
                            this.RemoveItemFromDependencyTracker(dependencyData.Key, dependencyData.NamedCache, dependencyData.RegionName, null);
                        }
                    }
                    finally
                    {
                        DependencyTrackerLocker.ExitWriteLock();
                    }

                    var cacheProviderErrorEventArgs = new CacheProviderErrorEventArgs()
                    {
                        Sender = this,
                        CacheKey = dependencyData == null ? "N/A" : dependencyData.Key,
                        ErrorMessage = "Fatal Exception for Query Notification subscription. Check the SQL Queries!",
                        State = dependencyData
                    };
                    if (this._cacheProvider != null)
                        this._cacheProvider.EventAggregatorInstance.GetEvent<CacheProviderErrorEvent>().Publish(cacheProviderErrorEventArgs);

                    break;
                default:
                    if (sender is OracleDependency)
                    {
                        CacheDependencyTracker cacheDependencyData;
                        var isDelayProcessing = false;
                        try
                        {
                            DependencyTrackerLocker.EnterReadLock();

                            cacheDependencyData = (from dependencyInfo in this._cacheDependencyTracker
                                                   where dependencyInfo.DependencyInstanceID.Equals((sender as OracleDependency).Id)
                                                   select dependencyInfo).FirstOrDefault();

                            if (cacheDependencyData != null)
                            {
                                var message = string.Format("Notification received By [{0}] for Cache with key [{1}] in regionName [{2}] under NamedCache [{3}]",
                                                            ConfigurationManager.AppSettings["ApplicationName"]
                                                            , cacheDependencyData.Key
                                                            , cacheDependencyData.RegionName
                                                            , cacheDependencyData.NamedCache);
                                if (this._logWriter != null)
                                    this._logWriter.Write(message, this._cacheProvider.LoggingCategory, 1, (int)LoggingEvent.CacheNotification, TraceEventType.Critical, cacheDependencyData.Key);

                                queryNotificationInfo = new QueryNotificationState
                                {
                                    Id = (sender as OracleDependency).Id
                                };

                                if (cacheDependencyData.DelayInDependencyNotificationProcessing != TimeSpan.Zero)
                                {
                                    var timer = new Timer(DependencyNotificationProcessingCallback, queryNotificationInfo,
                                                          cacheDependencyData.DelayInDependencyNotificationProcessing,
                                                          TimeSpan.Zero);

                                    queryNotificationInfo.Sender = timer;
                                    isDelayProcessing = true;
                                }
                            }
                        }

                        finally
                        {
                            DependencyTrackerLocker.ExitReadLock();
                        }


                        if (!isDelayProcessing && queryNotificationInfo != null)
                            this.DependencyNotificationProcessingCallback(queryNotificationInfo);
                    }
                    break;
            }
        }

        #endregion
    }
}
